/*
 * Main controller for Stopwatch app.
 *
 * For legal stuff see the file COPYRIGHT
 */
#import <stdio.h>
#import <ansi/string.h>		/* for import feature only (?) */
#import <bsd/sys/param.h>	/* for MAXPATHLEN */
#import <appkit/NXCType.h>
#import "Controller.h"
#import "StopWatch.h"
#import "InfoPanel.h"
#import "ClientInfo.h"
#import "ClientInspector.h"
#import "SessionEditor.h"
#import "AppIconView.h"
#import "createPath.h"
#import "Preferences.h"

#define PRIORITY	NX_MODALRESPTHRESHOLD

#define ARCHIVE_FILE	"client.data"
#define TEMPLATE_DIR	"Templates"
#define MAXCLIENTLEN	80
#define SELECTED_CLIENT "SelectedClient"

#define VERSION		2	/* the current file version that gets written */
int FileVersion;		/* the version of the file being read */

void
freeAndCopy( char **ptr, const char *str )
{
  if ( *ptr )
    free( *ptr );

  *ptr = NXCopyStringBuffer(str);
}

const char *
currentDate()
{
  time_t now;
  struct tm *tm;
  static char buf[10];

  time(&now);
  tm = localtime(&now);

  sprintf( buf, "%02d/%02d/%02d", tm->tm_mon + 1, tm->tm_mday, tm->tm_year );
  return buf;
}

const char *
currentTime()
{
  time_t now;
  struct tm *tm;
  static char buf[10];

  time(&now);
  tm = localtime(&now);

  sprintf( buf, "%02d:%02d", tm->tm_hour, tm->tm_min );
  return buf;
}

#define MAX_COMMA_STR 50		/* max digits we can handle */

void
commafy( int value, char *result )
{
  char *ptr, reverse[MAX_COMMA_STR + 1];
  int digits = 0, signum = 1;

  if ( value < 0 ) {
    signum = -1;		/* save the sign (and the whales) */
    value = -value;		/* make it positive for conversion */
  }

  ptr = reverse;

  do {
    if ( ++digits > 3 ) {
      digits = 1;		/* reset digit counter */
      *ptr++ = ',';		/* insert a comma */
    }

    *ptr++ = '0' + value % 10;	/* convert least significant digit to ascii */
    value /= 10;                /* and remove it from the number */

  } while ( value != 0 );       /* until no digits remain */

  if ( signum < 0 )
    *ptr++ = '-';               /* insert the negative sign */

  /* We already have a pointer to the end, so just copy backwards! */
  while ( ptr-- != reverse )
    *result++ = *ptr;
		
  *result = '\0';
}

/*
 * Puts commas into a double by convering to an int, after
 * adjusting for rounding and calling commafy()
 */
void
commafyDouble( double value, char *result )
{
  int signum = 1, cents;
  char buf[MAX_COMMA_STR + 1];

  if ( value < 0 ) {
    signum = -1;
    value *= -1;
  }

  value *= 100;
  value += 0.5;			/* Adjust for rounding */
  cents = (int)value % 100;	/* save the pennies for later */
  value /= 100;			/* chop off last two places */

  commafy( (int)(signum < 0 ? -value : value), buf );
  sprintf( result, "%s.%02d", buf, cents );
}

/*
 * To avoid having to exec /bin/cp.
 */
int
copyFile( const char *src, const char *dst )
{
  FILE *in, *out;
  int count;
  char buf[BUFSIZ];

  if ( ! (in = fopen( src, "r" ) ) ) {
    fprintf( stderr, "Can't open `%s' for reading.\n", src );
    return 0;
  }

  if ( ! (out = fopen( dst, "w" ) ) ) {
    fprintf( stderr, "Can't open `%s' for writing.\n", dst );
    fclose(in);
    return 0;
  }

  while ( (count = fread( buf, sizeof(char), sizeof(buf), in )) > 0 )
    fwrite( buf, sizeof(char), count, out );

  fclose(in);
  fclose(out);
  return 1;
}

@interface Controller(PRIVATE)
- (void)selectBrowserRow:(int)row;
- (ClientInfo *)findClient:(const char *)name;
- selectedClient;
- (int)selectedRow;
- (int)compare:obj1 :obj2;	/* comparison method for SortList */
- initInvoice;
- (void)addSession:(const char *)startDate
	      time:(const char *)startTime
	  duration:(int)minutes
       description:(const char *)desc;
- (void)checkStartButton;
@end


@implementation Controller

DPSTimedEntryProc 
showElapsedTime(DPSTimedEntry teNum, double now, char *data)
{
  [(id)data showElapsedTime];
  return (void *)NULL;
}

- (void) removeTimedEntry
{
  if ( teNum ) {
    DPSRemoveTimedEntry(teNum);
    teNum = 0 ;
  }
}

- (void) addTimedEntry
{
  [self removeTimedEntry];	/* in case there is one */
  
  /* Set it up so that the clock updates every minute */
  teNum = DPSAddTimedEntry( (double)60.0, (DPSTimedEntryProc)showElapsedTime,
			    (void *)self, PRIORITY );
}

- free
{
  [self removeTimedEntry];
  [stopwatch free];
  [infoPanel free];
  return [super free];
}

- awakeFromNib
{
  [window setFrameAutosaveName:"Stopwatch"];

  /* Make the browser's font match the startButton (can't do this in IB) */
  [[[browser matrixInColumn:0] prototype] setFont:[startButton font]];

  return self;
}

- add:sender
{
  [[ClientInspector sharedInstance] add:sender];
  return self;
}

- modify:sender
{
  [[ClientInspector sharedInstance] modify:sender];
  return self;
}

- delete:sender
{
  [[ClientInspector sharedInstance] delete:sender];
  return self;
}

- undelete:sender
{
  [[ClientInspector sharedInstance] undelete:sender];
  return self;
}

- (void)enableAdd:(BOOL)flag
{
  [addMenuItem setEnabled:flag];
}

- (void)enableModify:(BOOL)flag
{
  [modifyMenuItem setEnabled:flag];
}

- (void)enableDelete:(BOOL)flag
{
  [deleteButton setEnabled:flag];
}

- (void)enableUndelete:(BOOL)flag
{
  [undeleteButton setEnabled:flag];
}

/*
 * Redisplay from the data in the clientList. Try to re-select the
 * same item afterwards.
 */
- (void)decacheBrowser
{
  [browser loadColumnZero];
  [self selectBrowserRow:[clientList indexOf:activeClient]];
  [self checkStartButton];
}

- (NXTypedStream *)openArchive:(int)mode
{
  NXTypedStream *stream ;  

  if ( (stream = NXOpenTypedStreamForFile( filename, mode )) == NULL ) {
    NXRunAlertPanel( [NXApp appName], "Unable to open client data file: `%s'",
		    "Create it when needed", NULL, NULL, filename );
    return nil;
  }
  
  return stream;
}

/*
 * Read in the client info from the typestream file
 */
- (int)loadClientInfo
{
  NXTypedStream *stream ;

  if ( (stream = [self openArchive:NX_READONLY]) == nil )
    return 0;

  NXReadType( stream, "i", &FileVersion );
  [clientList read:stream];
  [clientList sort];
  NXCloseTypedStream(stream) ;

  return 1;
}

- (int)saveClientInfoToStream:(NXTypedStream *)stream
{
  int version = VERSION;
  
  NXWriteType( stream, "i", &version );
  [clientList write:stream];
  return 1;
}

- (int)saveClientInfo
{
  NXTypedStream *stream ;
  char backup[FILENAME_MAX + 1];

  /*
   * If this is the first write, move the old filename to
   * filename~ to serve as a backup.
   */
  if ( didBackup == NO ) {
    sprintf( backup, "%s~", filename );
    rename( filename, backup );
    didBackup = YES;
  }

  if ( (stream = [self openArchive:NX_WRITEONLY]) == nil )
    return 0;

  [self saveClientInfoToStream:stream];
  NXCloseTypedStream(stream);
  return 1;
}

/*
 * Edit the selected invoicing template by messaging to the
 * workspace to open the corresponding file. Sender is the
 * Matrix containing the menu of template names.
 */
- editTemplate:sender
{
  id cell = [sender cellAt:[sender selectedRow] :0];

  [self initInvoice];
  [invoice editTemplate:[cell title]];
  return self;
}

- preferences:sender
{
  [preferences display];
  return self;
}

- saveAs:sender
{
  SavePanel *savePanel = [SavePanel new];
  NXTypedStream *stream;
  const char *path;

  if ( [savePanel runModalForDirectory:dirname file:""] == 0 )
    return nil;

  path = [savePanel filename];

  if ( (stream = NXOpenTypedStreamForFile( path, NX_WRITEONLY )) == NULL ) {
    NXRunAlertPanel( [NXApp appName], "Unable to open file for writing: `%s'",
		    "What the...?", NULL, NULL, path );
    return nil;
  }
  
  [self saveClientInfoToStream:stream];
  NXCloseTypedStream(stream);
  
  return self;
}

- clientList
{
  return clientList;
}

/*
 * Select the client who was saved in the defaults db on our last exit.
 */
- (void)selectPreviousClient
{
  const char *name = NXGetDefaultValue( [NXApp appName], SELECTED_CLIENT );
  ClientInfo *client;

  if ( name && *name ) {
    client = [self findClient:name];
    [self selectBrowserRow:[clientList indexOf:client]];
    [self selectClient:self];
  }
}

- appDidInit:sender
{
  NXRect rect = {{0.0, 0.0}, {64.0, 64.0}};

  if ( createPath( dirname, DIRMODE ) != PathCreationOk ) {
    NXRunAlertPanel( [NXApp appName], "Cannot create path `%s'",
		    "Damned UNIX!", NULL, NULL, dirname );
    [NXApp terminate:sender];
  }

  preferences = [Preferences new];

  [self loadClientInfo];
  [self decacheBrowser];
  [self selectPreviousClient];

  if ( [preferences hideOnAutoLaunch] )
    [NXApp hide:self];
  else
    [window makeKeyAndOrderFront:self];

  /* make view that tracks elapsedTime be the appIcon window's contentView */
  appIconView = [[AppIconView alloc] initFrame:&rect 
	       		sourceView:elapsedTimeField];
  [[[NXApp appIcon] setContentView:appIconView] free];

  [browser setDoubleAction:@selector(inspect:)];
  [browser setTarget:self];

  /* If there are no clients defined yet, disable the start buttons */
  [self checkStartButton];

  return self;
}

/*
 * If we logout, or there's a powerOff,  make sure the time gets saved.
 */
- app:sender powerOffIn:(int)ms andSave:(int)aFlag
{
  return [self appWillTerminate:sender];
}

- appDidUnhide:sender
{
  [window makeKeyAndOrderFront:self];
  return self;
}

- appWillTerminate:sender
{
  ClientInfo *client = [self selectedClient];

  if ( teNum )
    [self stopClock];
		
  NXWriteDefault([NXApp appName], SELECTED_CLIENT, client ? [client shortName] : "");

  return self;
}

- init
{
  char path[FILENAME_MAX + 1];

  [super init];

  stopwatch = [[StopWatch alloc] init];

  clientList = [[SortList alloc] init];
  [clientList setAutoSort:YES];
  [clientList setDelegate:self];

  sprintf( path, "%s/Library/%s", NXHomeDirectory(), [NXApp appName] );
  dirname = NXCopyStringBuffer(path);

  sprintf( path, "%s/%s", dirname, ARCHIVE_FILE );
  filename = NXCopyStringBuffer(path);

  return self;
}

- (const char *) description
{
  return [description stringValue];
}

/*
 * Called once per minute by the timed entry routine while the clock is running.
 */
- showElapsedTime
{
  [elapsedTimeField setStringValue:[stopwatch elapsedTime]];
  [appIconView display];
  return self;
}

/*
 * Respond to the user's selection of a client
 */
- selectClient:sender
{
  /* Assume that this means we should stop the previous client */
  if ( [stopwatch running] == YES )
    [startButton performClick:sender];

  activeClient = [self selectedClient];
  [description setStringValue:[activeClient lastDescription]];
  [description selectText:sender];
  return self;
}

/*
 * The start button highlights, but we need to force the title to "Stop".
 * Setting the Alternate Title didn't seem to do the right thing in IB.
 */
- startClock
{
  id font = [elapsedTimeField font];

  [elapsedTimeField setFont:[[FontManager new] convertWeight:YES of:font]];
  [self addTimedEntry];
  [startButton   setTitle:"Stop"];
  [startMenuItem setTitle:"Stop"];
  [stopwatch startWatch];
  [self showElapsedTime];
  activeClient = [self selectedClient];
  return self;
}

/*
 * The mirror image of the above routine
 */
- stopClock
{
  id font = [elapsedTimeField font];
  [elapsedTimeField setFont:[[FontManager new] convertWeight:NO of:font]];
  [self removeTimedEntry];
  [startButton   setTitle:"Start"];
  [startMenuItem setTitle:"Start"];
  [stopwatch stopWatch];
  [self showElapsedTime];
  [self addSession:[stopwatch startDateString]
 	      time:[stopwatch startTimeString]
	  duration:[stopwatch elapsedMinutes]
       description:[self description]];
  activeClient = nil;
  return self;
}

/*
 * Called whenever the startButton is pressed.
 */
- buttonHandler:sender
{
  if ( [startButton state] == 1 )
    [self startClock];
  else
    [self stopClock];

  return self;
}

- showInfo:sender
{
  [[InfoPanel new] showInfo];
  return self;
}

/*
 * Inspect the currently selected client
 */
- inspect:sender
{
  Matrix *matrix = [browser matrixInColumn:0];
  ClientInspector *inspector = [ClientInspector sharedInstance];

  [inspector selectClientAt:[matrix selectedRow]];
  [inspector display];
  return self;
}

- inspectSessions:sender
{
  [[ClientInspector sharedInstance] showHours:sender];
  return self;
}

- inspectExpenses:sender
{
  [[ClientInspector sharedInstance] showExpenses:sender];
  return self;
}

- inspectClients:sender
{
  [[ClientInspector sharedInstance] showClient:sender];
  return self;
}

- generateDetail:sender
{
  [self initInvoice];
  [invoice generate:clientList];
  return self;
}

/*
 * Compact consecutive sessions with identical descriptions into
 * a single session with the same total time.
 */
- compactClients:sender
{
  int i, count = [clientList count];

  for ( i = 0; i < count; i++ )
    [[clientList objectAt:i] compactSessions];

  /* If it's showing, redisplay it, otherwise do nothing */
  [[ClientInspector sharedInstance] decacheView];

  [self saveClientInfo];
  return self;
}

/*
 * This needs to be cleaned up...
 */
- import:sender
{
  FILE *fp;
  const char *pathname;
  char buf[512], *tok;
  char shortName[80], startDate[80], startTime[80], minutes[80], desc[256];
  id openPanel = [OpenPanel new];
  ClientInspector *inspector = [ClientInspector sharedInstance];
  char delimiter[2], endDelimiters[10];

  if ( [openPanel runModal] == 0 )
    return nil;

  pathname = [openPanel filename];

  if ( ! (fp = fopen( pathname, "r" ) ) ) {
    NXRunAlertPanel( [NXApp appName], "Unable to open import file: `%s'",
		    "Eat me!", NULL, NULL, pathname );
    return self;
  }
  
  sprintf( delimiter,     "%c",   DELIMITER );
  sprintf( endDelimiters, "%c\n", DELIMITER );

  while ( fgets(buf, sizeof(buf), fp) ) {
    ClientInfo *info;
    Session *session;

    tok = strtok( buf, delimiter );
    strcpy( shortName, tok ) ;

    if ( ! (info = [self findClient:shortName]) ) {
      NXRunAlertPanel( [NXApp appName], "Ignoring unknown client: `%s'",
		      "Who needs 'em?", NULL, NULL, shortName );
      continue;
    }

    tok = strtok( NULL, delimiter );
    strcpy( startDate, tok );

    tok = strtok( NULL, delimiter );
    strcpy( startTime, tok );

    tok = strtok( NULL, delimiter );
    strcpy( minutes, tok );

    tok = strtok( NULL, endDelimiters ); /* throw out newline too.  */
    strcpy( desc, tok );

    session = [[Session alloc]
	   	init:startDate time:startTime
	   	duration:atoi(minutes) description:desc];

    [info addSession:session];
    [inspector updatedInfo:info];
  }

  fclose(fp);
  [self saveClientInfo];
  return self;
}

- export:sender
{
  FILE *fp;
  const char *pathname;
  int i, count = [clientList count];
  id savePanel = [SavePanel new];

  if ( [savePanel runModal] == 0 )
    return nil;

  pathname = [savePanel filename];

  if ( ! (fp = fopen( pathname, "w" ) ) ) {
    NXRunAlertPanel( [NXApp appName], "Unable to open export file: `%s'",
		    "I'll be darned!", NULL, NULL, pathname );
    return self;
  }
  
  for ( i = 0; i < count; i++ )
    [[clientList objectAt:i] exportToFile:fp];

  fclose(fp);
  return self;
}

/*
 * Clear out all session information from all clients.
 */
- closeMonth:sender
{
  ClientInspector *inspector = [ClientInspector sharedInstance];

  /* Give the user a chance to change their mind... */
  if ( NXRunAlertPanel( [NXApp appName], "Delete all session and expense data?",
		   "Delete all data", "Hell no!", NULL ) == NX_ALERTDEFAULT ) {
    [clientList makeObjectsPerform:@selector(deleteSessionsAndExpenses)];
    [inspector closeMonth];
    [self saveClientInfo];	/* write the newly empty file */
    [inspector decacheView];
  }

  return self;
}

@implementation Controller(PRIVATE)

- (void)selectBrowserRow:(int)row
{
  Matrix *matrix = [browser matrixInColumn:0];

  if ( row < 0 )
    row = 0;

  [matrix selectCellAt:row :0];
  [matrix scrollCellToVisible:row :0];  
}

/*
 * Find a client by short name
 */
- (ClientInfo *)findClient:(const char *)name
{
  int i, count = [clientList count];

  for ( i = 0; i < count; i++ ) {
    ClientInfo *info;

    info = [clientList objectAt:i];
    if ( strcmp( name, [info shortName] ) == 0 )
      return info ;
  }
  return nil;
}

- initInvoice
{
  char path[FILENAME_MAX + 1];

  if ( invoice == nil ) {
    sprintf( path, "%s/%s", dirname, TEMPLATE_DIR );
    invoice = [[Invoice alloc] initTemplateDir:path];
  }
  return invoice;
}

- (int)selectedRow
{
  return [[browser matrixInColumn:0] selectedRow];
}

- selectedClient
{
  return [clientList objectAt:[self selectedRow]];
}

/*
 * Make sure the start buttons are disabled if there are
 * no clients defined, and enabled if there are.
 */
- (void)checkStartButton
{
  BOOL flag = ( [clientList count] ? YES : NO );

  [startMenuItem setEnabled:flag];
  [startButton   setEnabled:flag];

  /* the same goes for these */
  [sessionMenuItem setEnabled:flag];
  [expenseMenuItem setEnabled:flag];

  /*
   * The only reasonable thing to do if there are no
   * clients is to create some!
   */
  if ( flag == NO )
    [self inspectClients:nil];
}

/*
 * Create a new session object and add it to the proper client's
 * ClientInfo list. Tell the browser what happened so it can update.
 */
- (void)addSession:(const char *)startDate
	      time:(const char *)startTime
	  duration:(int)minutes
       description:(const char *)desc
{
  Session *session = [[Session alloc]
		    	init:startDate time:startTime
       			duration:minutes description:desc];
  [activeClient addSession:session];
  [[ClientInspector sharedInstance] updatedInfo:activeClient];
  [self saveClientInfo];
}

/*
 * Compare two ClientInfo objects. Sort alpha by long name.
 */
- (int)compare:obj1 :obj2
{
  return strcmp( [obj1 clientName], [obj2 clientName] );
}

/*
 * Delegated method of NXBrowser.  This should be consolidated into a single
 * object.  Right now this method appears (almost) identically in the Controller
 * and the ClientMgr...  (Here we use the shortName instead of the full one.)
 */
- (int) browser:sender fillMatrix:matrix inColumn:(int)column
{
  int i, count = [clientList count];
  
  for ( i = 0; i < count; i++ ) {
    const char *name;
    id cell;
    
    [matrix addRow];
    name = [[clientList objectAt:i] shortName];
    cell = [matrix cellAt:i :0];	/* 1 dimen. matrix: always use col 0! */
    [cell setStringValue:name];
    [cell setLoaded:YES];
    [cell setLeaf:YES];
  }

  return count ;
}

@end
